
package gr.uoa.di.thanos.botcraft.game;

import java.io.File;
import java.io.IOException;
import java.nio.FloatBuffer;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
import java.util.UUID;

import javax.xml.XMLConstants;
import javax.xml.namespace.NamespaceContext;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.validation.SchemaFactory;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;

import org.lwjgl.BufferUtils;
import org.lwjgl.LWJGLException;
import org.lwjgl.opencl.CLContext;
import org.lwjgl.util.Renderable;
import org.w3c.dom.DOMException;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

import gr.uoa.di.thanos.botcraft.game.objects.Boulder;
import gr.uoa.di.thanos.botcraft.game.units.Factory;
import gr.uoa.di.thanos.botcraft.game.units.Worker;

/**
 * This class represents a game.
 * @author thanos.papapetrou@gmail.com
 *
 */
public class Game implements NamespaceContext, Renderable {
	private static final String SCHEMA = "/schemas/game.xsd";
	private static final String GAME_NS_URI = "http://www.di.uoa.gr/~thanos/botcraft/game/";
	private static final String GAME_NS_PREFIX = "game"; 
	
	private final Map map;
	private final Set<Object> objects;
	private final Set<Team> teams;
	
	/**
	 * Construct a new game.
	 * @param map the game map
	 * @param objects the game objects
	 * @param teams the game teams
	 */
	public Game(final Map map, final Set<Object> objects, final Set<Team> teams) {
		if (map == null)
			throw new IllegalArgumentException(Game.class.getName() + " map can not be null");
		if (objects == null)
			throw new IllegalArgumentException(Game.class.getName() + " objects can not be null");
		if (teams == null)
			throw new IllegalArgumentException(Game.class.getName() + " teams can not be null");
		this.map = map;
		this.objects = objects;
		this.teams = teams;
	}
	
	/**
	 * Construct a new game by loading it from a file.
	 * @param file the game file
	 * @param context the OpenCL context to use for map calculations
	 * @throws IOException if any errors occur while loading
	 * @throws LWJGLException 
	 * @throws DOMException 
	 * @throws NumberFormatException 
	 */
	public Game(final File file) throws IOException, NumberFormatException, DOMException, LWJGLException {
		try {
			final DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
			documentBuilderFactory.setNamespaceAware(true);
			documentBuilderFactory.setSchema(SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI).newSchema(Game.class.getResource(SCHEMA)));
			documentBuilderFactory.setXIncludeAware(true);
			final Document document = documentBuilderFactory.newDocumentBuilder().parse(file);
			final XPath xPath = XPathFactory.newInstance().newXPath();
			xPath.setNamespaceContext(this);
			final Node gameNode = (Node) xPath.evaluate("/botcraft:game", document, XPathConstants.NODE);			
			final Node mapNode = (Node) xPath.evaluate("./botcraft:map", gameNode, XPathConstants.NODE);
			final NodeList tileNodes = (NodeList) xPath.evaluate("./botcraft:tile", mapNode, XPathConstants.NODESET);
			final FloatBuffer altitudes = BufferUtils.createFloatBuffer(tileNodes.getLength());
			final Terrain[] terrains = new Terrain[tileNodes.getLength()];
			for (int i = 0; i < tileNodes.getLength(); i++) {
				altitudes.put(i, Float.parseFloat(((Node) xPath.evaluate("./botcraft:altitude", tileNodes.item(i), XPathConstants.NODE)).getTextContent()));
				terrains[i] = Terrain.valueOf(((Node) xPath.evaluate("./botcraft:terrain", tileNodes.item(i), XPathConstants.NODE)).getTextContent());
			}			
			map = new Map(Integer.parseInt(((Node) xPath.evaluate("./botcraft:length", mapNode, XPathConstants.NODE)).getTextContent()),
					Integer.parseInt(((Node) xPath.evaluate("./botcraft:width", mapNode, XPathConstants.NODE)).getTextContent()), altitudes, terrains);
			final NodeList objectNodes = (NodeList) xPath.evaluate("./botcraft:object", gameNode, XPathConstants.NODESET);
			objects = new HashSet<Object>();
			for (int i = 0; i < objectNodes.getLength(); i++) {
				final String clazz = ((Node) xPath.evaluate("./botcraft:class", objectNodes.item(i), XPathConstants.NODE)).getTextContent();
				final UUID id = UUID.fromString(((Node) xPath.evaluate("./botcraft:id", objectNodes.item(i), XPathConstants.NODE)).getTextContent());
				final Node positionNode = (Node) xPath.evaluate("./botcraft:position", objectNodes.item(i), XPathConstants.NODE);
				final Vector position = new Vector(Float.parseFloat(((Node) xPath.evaluate("./botcraft:x", positionNode, XPathConstants.NODE)).getTextContent()),
						Float.parseFloat(((Node) xPath.evaluate("./botcraft:x", positionNode, XPathConstants.NODE)).getTextContent()),
						Float.parseFloat(((Node) xPath.evaluate("./botcraft:x", positionNode, XPathConstants.NODE)).getTextContent()));
				final Node orientationNode = (Node) xPath.evaluate("./botcraft:orientation", objectNodes.item(i), XPathConstants.NODE);
				final Vector orientation = new Vector(Float.parseFloat(((Node) xPath.evaluate("./botcraft:x", orientationNode, XPathConstants.NODE)).getTextContent()),
						Float.parseFloat(((Node) xPath.evaluate("./botcraft:x", orientationNode, XPathConstants.NODE)).getTextContent()),
						Float.parseFloat(((Node) xPath.evaluate("./botcraft:x", orientationNode, XPathConstants.NODE)).getTextContent()));
				if (clazz.equals(Boulder.class.getName()))
					objects.add(new Boulder(id, position, orientation));
				else if (clazz.equals(Factory.class.getName()))
					objects.add(new Factory(id, position, orientation));
				else if (clazz.equals(Worker.class.getName()))
					objects.add(new Worker(id, position, orientation));
			}
			final NodeList teamNodes = (NodeList) xPath.evaluate("./botcraft:team", gameNode, XPathConstants.NODESET);
			teams = new HashSet<Team>();
			for (int i = 0; i < teamNodes.getLength(); i++) {
				final NodeList playerNodes = (NodeList) xPath.evaluate("./botcraft:player", teamNodes.item(i), XPathConstants.NODE);
				final Set<Player> players = new HashSet<Player>();
				for (int j = 0; j < playerNodes.getLength(); j++) {
					final NodeList unitNodes = (NodeList) xPath.evaluate("./botcraft:unit", playerNodes.item(j), XPathConstants.NODESET);
					final Set<Unit> units = new HashSet<Unit>();
					for (int k = 0; k < unitNodes.getLength(); k++) {
						final String clazz = ((Node) xPath.evaluate("./botcraft:class", unitNodes.item(k), XPathConstants.NODE)).getTextContent();
						final UUID id = UUID.fromString(((Node) xPath.evaluate("./botcraft:id", unitNodes.item(k), XPathConstants.NODE)).getTextContent());
						final Node positionNode = (Node) xPath.evaluate("./botcraft:position", unitNodes.item(k), XPathConstants.NODE);
						final Vector position = new Vector(Float.parseFloat(((Node) xPath.evaluate("./botcraft:x", positionNode, XPathConstants.NODE)).getTextContent()),
								Float.parseFloat(((Node) xPath.evaluate("./botcraft:x", positionNode, XPathConstants.NODE)).getTextContent()),
								Float.parseFloat(((Node) xPath.evaluate("./botcraft:x", positionNode, XPathConstants.NODE)).getTextContent()));
						final Node orientationNode = (Node) xPath.evaluate("./botcraft:orientation", objectNodes.item(i), XPathConstants.NODE);
						final Vector orientation = new Vector(Float.parseFloat(((Node) xPath.evaluate("./botcraft:x", orientationNode, XPathConstants.NODE)).getTextContent()),
								Float.parseFloat(((Node) xPath.evaluate("./botcraft:x", orientationNode, XPathConstants.NODE)).getTextContent()),
								Float.parseFloat(((Node) xPath.evaluate("./botcraft:x", orientationNode, XPathConstants.NODE)).getTextContent()));
						if (clazz.equals(Factory.class.getName()))
							units.add(new Factory(id, position, orientation));
						else if (clazz.equals(Worker.class.getName()))
							units.add(new Worker(id, position, orientation));
					}
					players.add(new Player(((Node) xPath.evaluate("./botcraft:name", playerNodes.item(j), XPathConstants.NODE)).getTextContent(), units));
				}
				teams.add(new Team(((Node) xPath.evaluate("./botcraft:name", teamNodes.item(i), XPathConstants.NODE)).getTextContent(), players));
			}
		} catch (final ParserConfigurationException e) {
			throw new IOException("Error loading game from file " + file, e);
		} catch (final SAXException e) {
			throw new IOException("Error loading game from file " + file, e);
		} catch (final XPathExpressionException e) {
			throw new IOException("Error loading game from file " + file, e);
		}
	}
	
	public void initialize(final CLContext context, final boolean shareGlClContext) {
		map.initialize(context, shareGlClContext);
	}
	
	/**
	 * Get the game map
	 * @return the game map
	 */
	public Map getMap() {
		return map;
	}
	
	/**
	 * Get the game objects.
	 * @return the game objects
	 */
	public Set<Object> getObjects() {
		return objects;
	}
	
	/**
	 * Get the game teams.
	 * @return the game teams
	 */
	public Set<Team> getTeams() {
		return teams;
	}
	
	/**
	 * Save this game to a file.
	 * @param file the file to save the game to
	 * @throws IOException if any errors occur while saving
	 */
	public void save(final File file) throws IOException {
		try {
			final DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
			documentBuilderFactory.setNamespaceAware(true);
			documentBuilderFactory.setSchema(SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI).newSchema(Game.class.getResource(SCHEMA)));
			documentBuilderFactory.setXIncludeAware(true);
			final DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder();
			final Document document = documentBuilder.newDocument();
			final Element gameNode = document.createElementNS(GAME_NS_URI, "botcraft:game");
			gameNode.setAttributeNS(XMLConstants.W3C_XML_SCHEMA_INSTANCE_NS_URI, "xsi:schemaLocation", GAME_NS_URI + " " + SCHEMA);
			final Element mapNode = document.createElementNS(GAME_NS_URI, "botcraft:map");
			final Element lengthNode = document.createElementNS(GAME_NS_URI, "botcraft:length");
			lengthNode.appendChild(document.createTextNode(Integer.toString(map.getLatitudinalSize())));
			mapNode.appendChild(lengthNode);
			final Element widthNode = document.createElementNS(GAME_NS_URI, "botcraft:width");
			widthNode.appendChild(document.createTextNode(Integer.toString(map.getLongitudinalSize())));
			mapNode.appendChild(widthNode);
			for (int i = 0; i < map.getLatitudinalSize() * map.getLongitudinalSize(); i++) {
				final Element tileNode = document.createElementNS(GAME_NS_URI, "botcraft:tile");
				final Element altitudeNode = document.createElementNS(GAME_NS_URI, "botcraft:altitude");
				altitudeNode.appendChild(document.createTextNode(Float.toString(map.getAltitudes().get(i))));
				tileNode.appendChild(altitudeNode);
				final Element terrainNode = document.createElementNS(GAME_NS_URI, "botcraft:terrain");
				terrainNode.appendChild(document.createTextNode(map.getTerrains()[i].name()));
				tileNode.appendChild(terrainNode);
				mapNode.appendChild(tileNode);
			}
			gameNode.appendChild(mapNode);
			for (Object object : objects) {
				final Element objectNode = document.createElementNS(GAME_NS_URI, "botraft:object");
				final Element classNode = document.createElementNS(GAME_NS_URI, "botcraft:class");
				classNode.appendChild(document.createTextNode(object.getClass().getName()));
				objectNode.appendChild(classNode);
				final Element idNode = document.createElementNS(GAME_NS_URI, "botcraft:id");
				idNode.appendChild(document.createTextNode(object.getId().toString()));
				objectNode.appendChild(idNode);
				final Element positionNode = document.createElementNS(GAME_NS_URI, "botcraft:position");
				final Element positionXNode = document.createElementNS(GAME_NS_URI, "botcraft:x");
				positionXNode.appendChild(document.createTextNode(Float.toString(object.getPosition().getX())));
				positionNode.appendChild(positionXNode);
				final Element positionYNode = document.createElementNS(GAME_NS_URI, "botcraft:y");
				positionYNode.appendChild(document.createTextNode(Float.toString(object.getPosition().getY())));
				positionNode.appendChild(positionYNode);
				final Element positionZNode = document.createElementNS(GAME_NS_URI, "botcraft:z");
				positionZNode.appendChild(document.createTextNode(Float.toString(object.getPosition().getZ())));
				positionNode.appendChild(positionZNode);
				objectNode.appendChild(positionNode);
				final Element orientationNode = document.createElementNS(GAME_NS_URI, "botcraft:orientation");
				final Element orientationXNode = document.createElementNS(GAME_NS_URI, "botcraft:x");
				orientationXNode.appendChild(document.createTextNode(Float.toString(object.getPosition().getX())));
				orientationNode.appendChild(orientationXNode);
				final Element orientationYNode = document.createElementNS(GAME_NS_URI, "botcraft:y");
				orientationYNode.appendChild(document.createTextNode(Float.toString(object.getPosition().getY())));
				orientationNode.appendChild(orientationYNode);
				final Element orientationZNode = document.createElementNS(GAME_NS_URI, "botcraft:z");
				orientationZNode.appendChild(document.createTextNode(Float.toString(object.getPosition().getZ())));
				orientationNode.appendChild(orientationZNode);
				objectNode.appendChild(orientationNode);
				gameNode.appendChild(objectNode);
			}
			for (Team team : teams) {
				final Element teamNode = document.createElementNS(GAME_NS_URI, "botcraft:team");
				final Element teamNameNode = document.createElementNS(GAME_NS_URI, "botcraft:name");
				teamNameNode.appendChild(document.createTextNode(team.getName()));
				teamNode.appendChild(teamNameNode);
				for (Player player : team.getPlayers()) {
					final Element playerNode = document.createElementNS(GAME_NS_URI, "botcraft:player");
					final Element playerNameNode = document.createElementNS(GAME_NS_URI, "botcraft:name");
					playerNameNode.appendChild(document.createTextNode(player.getName()));
					playerNode.appendChild(playerNameNode);
					for (Unit unit : player.getUnits()) {
						final Element unitNode = document.createElementNS(GAME_NS_URI, "botcraft:unit");
						final Element classNode = document.createElementNS(GAME_NS_URI, "botcraft:class");
						classNode.appendChild(document.createTextNode(unit.getClass().getName()));
						unitNode.appendChild(classNode);
						final Element idNode = document.createElementNS(GAME_NS_URI, "botcraft:id");
						idNode.appendChild(document.createTextNode(unit.getId().toString()));
						unitNode.appendChild(idNode);
						final Element positionNode = document.createElementNS(GAME_NS_URI, "botcraft:position");
						final Element positionXNode = document.createElementNS(GAME_NS_URI, "botcraft:x");
						positionXNode.appendChild(document.createTextNode(Float.toString(unit.getPosition().getX())));
						positionNode.appendChild(positionXNode);
						final Element positionYNode = document.createElementNS(GAME_NS_URI, "botcraft:y");
						positionYNode.appendChild(document.createTextNode(Float.toString(unit.getPosition().getY())));
						positionNode.appendChild(positionYNode);
						final Element positionZNode = document.createElementNS(GAME_NS_URI, "botcraft:z");
						positionZNode.appendChild(document.createTextNode(Float.toString(unit.getPosition().getZ())));
						positionNode.appendChild(positionZNode);
						unitNode.appendChild(positionNode);
						final Element orientationNode = document.createElementNS(GAME_NS_URI, "botcraft:orientation");
						final Element orientationXNode = document.createElementNS(GAME_NS_URI, "botcraft:x");
						orientationXNode.appendChild(document.createTextNode(Float.toString(unit.getOrientation().getX())));
						orientationNode.appendChild(orientationXNode);
						final Element orientationYNode = document.createElementNS(GAME_NS_URI, "botcraft:y");
						orientationYNode.appendChild(document.createTextNode(Float.toString(unit.getOrientation().getY())));
						orientationNode.appendChild(orientationYNode);
						final Element orientationZNode = document.createElementNS(GAME_NS_URI, "botcraft:z");
						orientationZNode.appendChild(document.createTextNode(Float.toString(unit.getOrientation().getZ())));
						orientationNode.appendChild(orientationZNode);
						unitNode.appendChild(orientationNode);
						playerNode.appendChild(unitNode);
					}
					teamNode.appendChild(playerNode);
				}
				gameNode.appendChild(teamNode);
			}
			document.appendChild(gameNode);
			TransformerFactory.newInstance().newTransformer().transform(new DOMSource(document), new StreamResult(file));
		} catch (final ParserConfigurationException e) {
			throw new IOException("Error saving game to file " + file, e);
		} catch (final SAXException e) {
			throw new IOException("Error saving game to file " + file, e);
		} catch (final TransformerException e) {
			throw new IOException("Error saving game to file " + file, e);
		}
	}
	
	public void dispose() {
		map.dispose();
	}
	
	@Override
	public void render() {
		map.render();
	}
		
	@Override
	public String getNamespaceURI(final String prefix) {
		if (prefix == null)
			throw new IllegalArgumentException("Namespace prefix can not be null");
		else if (prefix.equals(XMLConstants.XML_NS_PREFIX))
			return XMLConstants.XML_NS_URI;
		else if (prefix.equals(XMLConstants.XMLNS_ATTRIBUTE))
			return XMLConstants.XMLNS_ATTRIBUTE_NS_URI;
		else if (prefix.equals(GAME_NS_PREFIX))
			return GAME_NS_URI;
		else
			return XMLConstants.NULL_NS_URI;
	}
	
	@Override
	public String getPrefix(final String uri) {
		if (uri == null)
			throw new IllegalArgumentException("Namespace URI can not be null");
		else if (uri.equals(XMLConstants.XML_NS_URI))
			return XMLConstants.XML_NS_PREFIX;
		else if (uri.equals(XMLConstants.XMLNS_ATTRIBUTE_NS_URI))
			return XMLConstants.XMLNS_ATTRIBUTE;
		else if (uri.equals(GAME_NS_URI))
			return GAME_NS_PREFIX;
		else
			return null;
	}

	@Override
	public Iterator<String> getPrefixes(final String uri) {
		return Collections.singleton(getPrefix(uri)).iterator();
	}
}
